#include "stdafx.h"
#include "VideoDevice.h"

#include "Log.h"
#include "Exception.h"
#include "ErrorHandlers.h"
#include "MemUtils.h"
#include "Engine.h"
#include "Timer.h"

namespace Cvekas {

VideoDevice::VideoDevice()
: clear_color(0, 0, 0, 0), switch_mode(false), in_scene_block(false), lost(false), frame_ended(false),
  default_rt(NULL), default_ds(NULL)
{
	IDirect3D9* d3d;
	
	d3d = Direct3DCreate9(D3D_SDK_VERSION);
	
	if(d3d == NULL)
		throw EXCEPTION("Failed to create Direct3D object");
	LOG("Direct3D initialized");
	direct3d = d3d;
	
	D3DADAPTER_IDENTIFIER9 id;
	E_E(direct3d->GetAdapterIdentifier(D3DADAPTER_DEFAULT, NULL, &id));
	LOG(FORMAT("Adapter description: %s", % id.Description));
	
	int product = HIWORD(id.DriverVersion.HighPart);
    int version = LOWORD(id.DriverVersion.HighPart);
    int subversion = HIWORD(id.DriverVersion.LowPart);
    int build = LOWORD(id.DriverVersion.LowPart);
	
	LOG(FORMAT("Driver: %s %d.%d.%d.%d", % id.Driver % product % version % subversion % build));
}

VideoDevice::~VideoDevice()
{
	safeRelease(default_rt);
	safeRelease(default_ds);

	// TODO: warn if shared_ptrs about to be resetted have refcount > 1
	effect_manager.reset();
	gpu_buffer_manager.reset();
	texture_manager.reset();

	LOG("Releasing device");
	intrusivePtrRelease(device);
	LOG("Releasing direct3d");
	intrusivePtrRelease(direct3d);
	LOG("VideoDevice destroyed");
}

DisplayModesPtr VideoDevice::getModes()
{
	// If display_modes is not constructed yet...
	if(!bool(display_modes))
		display_modes = DisplayModesPtr(new DisplayModes(direct3d));

	return display_modes;
}

DeviceCapsPtr VideoDevice::getCaps(D3DDEVTYPE device_type)
{
	// If caps is not filled...
	if(!bool(device_caps))
		device_caps = DeviceCapsPtr(new DeviceCaps(direct3d, device_type));

	// Or filled for wrong device type...
	if(device_caps->device_type != device_type)
	{
		device_caps.reset();
		device_caps = DeviceCapsPtr(new DeviceCaps(direct3d, device_type));
	}
	return device_caps;
}

void VideoDevice::init(WindowPtr window)
{
	LOG("Creating device...");
	
	this->window = window;
	
	setUpParams();

	VideoSettings& settings = getSettings();

	D3DDEVTYPE device_type = settings.getRenderer();
	DWORD behavior = settings.getVertexProcessing();
	
	IDirect3DDevice9* dummy_device;
	E_E(direct3d->CreateDevice(D3DADAPTER_DEFAULT, device_type, window->getHandle(), behavior, &params, &dummy_device));
	device = dummy_device;

	texture_manager = TextureManagerPtr(new TextureManager(device));
	gpu_buffer_manager = GpuBufferManagerPtr(new GpuBufferManager(device));
	effect_manager = EffectManagerPtr(new EffectManager(device));

	E_E(device->GetRenderTarget(0, &default_rt));
	E_E(device->GetDepthStencilSurface(&default_ds));

	viewport.X = 0;
	viewport.Y = 0;
	viewport.Width = settings["ScreenWidth"];
	viewport.Height = settings["ScreenWidth"];
	viewport.MinZ = 0.0f;
	viewport.MaxZ = 1.0f;

	//device->ShowCursor(false);

	LOG("Device successfully created");
}

VideoSettings& VideoDevice::getSettings() 
{ 
	Engine e; 
	return static_cast<VideoSettings&>((*e.getSettings())["video"]); 
};

void VideoDevice::setClearColor(const Color& color)
{
	clear_color = (D3DXCOLOR)color;
}

void VideoDevice::getViewport(uint& width, uint& height)
{
	width = viewport.Width;
	height = viewport.Height;
}

void VideoDevice::switchMode()
{
	// TODO: don't switch modes if settings not changed

	if(in_scene_block)
	{
		switch_mode = true;
		return;
	}

	// TODO: Print from/to mode names
	LOG("Switching screen mode");

	reset();
	window->switchMode();

	switch_mode = false;
}

bool VideoDevice::ensureDevice()
{
	HRESULT device_state = device->TestCooperativeLevel();

	if(D3D_OK == device_state)
		return true;

	if(D3DERR_DRIVERINTERNALERROR == device_state)
	{
		throw EXCEPTION("Internal driver error! Quitting application");
	}

	if(D3DERR_DEVICELOST == device_state)
	{
		if(!lost)
		{
			lost = true;
			deviceLost();
		}
	}

	if(D3DERR_DEVICENOTRESET == device_state)
	{
		reset();
	}
	return false;
}

void VideoDevice::beginScene(bool clear_color, bool clear_depth, bool clear_stencil, 
							 RenderTargetPtr render_target)
{
	if(in_scene_block)
		throw EXCEPTION("Already inside scene!");
	in_scene_block = true;

	if(frame_ended)
	{
		frame_ended = false;
		rendering_params.setPerFrame(float(Timer::getTimeMs()) / 1000.0f);
	}

	DWORD flags = 0;
	if(clear_color)
		flags |= D3DCLEAR_TARGET;
	if(clear_depth)
		flags |= D3DCLEAR_ZBUFFER;
	if(clear_stencil)
		flags |= D3DCLEAR_STENCIL;

	static bool disabled_depth = false;

	if(bool(render_target))
	{
		E_E(device->SetRenderTarget(0, render_target->getD3DSurface()));
		E_E(device->Clear(0, NULL, flags, this->clear_color, 1.0f, 0));
		
		const D3DSURFACE_DESC& desc = render_target->getTopLevelDescription();
		
		float width = float(desc.Width);
		float height = float(desc.Height);

		// TODO: Fix this
		// Disable depth buffer if bigger than screen
		D3DSURFACE_DESC depth_desc;
		E_E(default_ds->GetDesc(&depth_desc));
		if(depth_desc.Width < desc.Width || depth_desc.Height < desc.Height)
		{
			if(!disabled_depth)
			{
				E_E(device->SetDepthStencilSurface(NULL));
				disabled_depth = true;
			}
		}
		else
		{
			if(disabled_depth)
			{
				E_E(device->SetDepthStencilSurface(default_ds));
				disabled_depth = false;
			}
		}

		rendering_params.setPerScene(projection_matrix, Vector2(width, height));
	}
	else
	{
		if(disabled_depth)
		{
			E_E(device->SetDepthStencilSurface(default_ds));
			disabled_depth = false;
		}
		E_E(device->SetRenderTarget(0, default_rt));
		E_E(device->Clear(0, NULL, flags, this->clear_color, 1.0f, 0));
		E_E(device->SetViewport(&viewport));

		float width = float(params.BackBufferWidth);
		float height = float(params.BackBufferHeight);

		rendering_params.setPerScene(projection_matrix, Vector2(width, height));
	}

	E_E(device->BeginScene());
}

void VideoDevice::endScene()
{
	if(!in_scene_block)
		throw EXCEPTION("Not in scene!");
	in_scene_block = false;

	E_E(device->EndScene());

	IDirect3DSurface9* rt;

	E_E(device->GetRenderTarget(0, &rt));
	if(rt == default_rt)
	{
		E_E(device->Present(NULL, NULL, NULL, NULL));
		frame_ended = true;

		if(switch_mode)
		{
			safeRelease(rt);
			switchMode();
			return;
		}
	}
	safeRelease(rt);
}

TextureManagerPtr VideoDevice::getTextureManager() const
{
	return texture_manager;
}

GpuBufferManagerPtr VideoDevice::getGpuBufferManager() const
{
	return gpu_buffer_manager;
}

EffectManagerPtr VideoDevice::getEffectManager() const
{
	return effect_manager;
}

SpriteBatchPtr VideoDevice::createSpriteBatch(RendererPtr renderer, uint virtual_width, uint virtual_height, uint16 priority)
{
	return SpriteBatchPtr(new SpriteBatch(renderer, virtual_width, virtual_height, priority));
}

RendererPtr VideoDevice::createRenderer()
{
	return RendererPtr(new Renderer(device, effect_manager, gpu_buffer_manager));
}

void VideoDevice::deviceLost()
{
	LOG("Device lost.");
	LOG("Notifying TextureManager");
	texture_manager->onDeviceLost();
	LOG("Notifying GpuBufferManager");
	gpu_buffer_manager->onDeviceLost();
	LOG("Notifying EffectManager");
	effect_manager->onDeviceLost();
}

void VideoDevice::deviceReset()
{
	LOG("Device reset."); 
	LOG("Notifying TextureManager");
	texture_manager->onDeviceReset();
	LOG("Notifying GpuBufferManager");
	gpu_buffer_manager->onDeviceReset();
	LOG("Notifying EffectManager");
	effect_manager->onDeviceReset();
}

void VideoDevice::setUpParams()
{
	ZeroMemory(&params, sizeof(params));

	VideoSettings& settings = getSettings();

	bool fullscreen = settings["Fullscreen"];

	params.AutoDepthStencilFormat = settings.getDepthStencilFormat();
	params.BackBufferCount = 1;
	if(fullscreen)
		params.BackBufferFormat = settings.getBBufferFormat();
	else
		params.BackBufferFormat = D3DFMT_UNKNOWN;
	params.BackBufferHeight = settings["ScreenHeight"];
	params.BackBufferWidth = settings["ScreenWidth"];
	params.EnableAutoDepthStencil = true;
	//params.Flags = ???
	if(fullscreen)
		params.FullScreen_RefreshRateInHz = settings["RefreshRate"];
	else
		params.FullScreen_RefreshRateInHz = 0;

	params.hDeviceWindow = window->getHandle();
	params.MultiSampleQuality = 0;
	params.MultiSampleType = settings.getAntialiasing();
	params.PresentationInterval = settings.getPresentInterval();
	params.SwapEffect = D3DSWAPEFFECT_DISCARD;
	params.Windowed = !fullscreen;
}

const Matrix& VideoDevice::getProjectionMatrix() const
{
	if(in_scene_block)
		return rendering_params.projection;

	return projection_matrix;
}

void VideoDevice::setAspectRatio(float ratio)
{
	D3DSURFACE_DESC desc;
	E_E(default_rt->GetDesc(&desc));
	uint width = desc.Width;
	uint height = desc.Height;

	float current_ratio = (float)width/(float)height;
	if(FCMP(current_ratio, ratio))
		return;

	// Letterbox by setting viewport
	uint new_width = width, new_height = height;
	if(current_ratio > ratio)
		new_width = (uint)((float)height * (float)ratio);
	if(current_ratio < ratio)
		new_height = (uint)((float)width / (float)ratio);

	//D3DVIEWPORT9 viewport;
	viewport.X = (width - new_width) / 2;
	viewport.Y = (height - new_height) / 2;
	viewport.Width = new_width;
	viewport.Height = new_height;
	viewport.MinZ = 0.0f;
	viewport.MaxZ = 1.0f;

	E_E(device->Clear(0, NULL, D3DCLEAR_TARGET, 0, 1.0f, 0));
	E_E(device->SetViewport(&viewport));
}

void VideoDevice::reset()
{
	setUpParams();

	if(!lost)
	{
		lost = true;
		deviceLost();
	}

	safeRelease(default_rt);
	safeRelease(default_ds);

	LOG("Resetting device");

	// HACK: Should not ignore result
	E_E(device->Reset(&params));
	deviceReset();
	lost = false;

	E_E(device->GetRenderTarget(0, &default_rt));
	E_E(device->GetDepthStencilSurface(&default_ds));
}

} // namespace
